好好学习,天天向上

译|Python的隐藏特性(上)

知乎上有人问了一个问题:Python有哪些新手不会了解的深入细节。 其中的一个答案引用了stackoverflow上的一个问题解答。 鉴于一直在努力摆脱Python小白,决定好好研究下这几个特性,顺手翻译一下下,扩展一下下~~

原文:Hidden features of Python

Argument Unpacking

可以使用*和**分别将一个列表或一个字典解包为函数参数。 例如:

1
2
3
4
5
6
7
8
def draw_point(x, y):
# do some magic

point_foo = (3, 4)
point_bar = {'y': 3, 'x': 2}

draw_point(*point_foo)
draw_point(**point_bar)
由于列表、元组和字典被广泛作为容器使用,因此这是一个非常有用的捷径。 ### 碎碎念 看了下面的评论,发现这个特性广为大家喜爱呀~~ 比较悲催的是,Pylint貌似不大喜欢它。 "*"这个东东也叫作"splat"操作符。鉴于搜索一般会对某些字符当做特殊字符处理,因此,记着英文名也好找些。

Braces

假如你不喜欢用空格来表示范围,那么可以通过下面的方式来使用C风格的{}

1
from __future__ import braces
### 碎碎念 其实这个是个复活节彩蛋啦。 当输入这句话的时候,会得到一个错误SyntaxError: not a chance (<pyshell#1>, line 2) 这说明,这个特性永远不可能实现的。 Python开发者超有幽默感的,你可以试试输入例如import __hello__, import this, import antigravity看看会发生啥事~~

Chaining Comparison Operators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> x = 5
>>> 1 < x < 10
True
>>> 10 < x < 20
False
>>> x < 10 < x*10 < 100
True
>>> 10 > x <= 9
True
>>> 5 == x > 4
True
>>> 10 < x < 20
False
>>> (5 in [5] is True)
False

如果你是这样想的:首先1 < x,结果是True,然后比较True < 10,结果也是True。那么,不好意思,你错了哦,事实根本不是这样的好不好。它会转换为1 < x and x < 10,和x < 10 and 10 < x * 10 and x*10 < 100。当然,这样就不需要输入那么多东东,而且每一个语句只会被计算一次。

碎碎念

Lisp貌似也是这样玩的。 然后,上面最后例子为嘛是False呢?其实你可以理解为5 in [5] and [5] is True,这样,就知道为嘛了~~

Decorators

装饰器允许在另一个函数中包装一个函数或方法,这样可以增加功能,修改参数或结果等。在函数定义的上面一行写上装饰器,以"at"(@)标志开头。 下面的例子展示了一个print_args装饰器,它在调用函数前打印所装饰的函数参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def print_args(function):
>>> def wrapper(*args, **kwargs):
>>> print 'Arguments:', args, kwargs
>>> return function(*args, **kwargs)
>>> return wrapper

>>> @print_args
>>> def write(text):
>>> print text

>>> write('foo')
Arguments: ('foo',) {}
foo
### 碎碎念 为什么要使用装饰器而不是另外定义一个函数,然后在这个函数中增加可选参数呢? 答案就是,如果你想增加的功能是一个通用功能呢?总不能为每一个用到这个功能的函数都增加一个新的函数吧?而且,只要短短一句话,就可以将它附加到任意一个函数上哦。例如,当你想要调试的时候,就可以直接加上那么一句,调试完就可以直接移除啦。

Default Argument Gotchas / Dangers of Mutable Default arguments

要小心可变的默认参数哦~~

1
2
3
4
5
6
7
8
9
10
>>> def foo(x=[]):
... x.append(1)
... print x
...
>>> foo()
[1]
>>> foo()
[1, 1]
>>> foo()
[1, 1, 1]
相反,你应该使用一个警戒值来表示“未提供”,然后将可变参数的默认值替换成为这个警戒值:
1
2
3
4
5
6
7
8
9
>>> def foo(x=None):
... if x is None:
... x = []
... x.append(1)
... print x
>>> foo()
[1]
>>> foo()
[1]
### 碎碎念 这个属性藏得很深呢,估计给不少人埋了雷。 至于要怎么理解呢?我们强悍的小伙伴这样解释: 因为def是一个可执行语句,只有def执行的时候才会计算默认默认参数的值,所以使用默认参数会造成函数执行的时候一直在使用同一个对象,引起bug。

另外,上面第二个例子可以改写为

1
2
3
4
def foo(x=None):
x = x or [] # 或者 x = [] if x is None
x.append(1)
print x
在这里,可以跟lambda来对比一下。考虑下面的例子:
1
2
3
4
5
6
7
8
>>> x = 10
>>> a = lambda y:x+y
>>> x = 20
>>> b = lambda y:x+y
>>> a(10)
30
>>> b(10)
30
结果是不是很出乎意料? 这是因为,跟函数的默认值参数定义不同,这里lambda表达式中的x是一个自由变量,在运行时绑定值,而不是像函数的默认值参数是在定义时就绑定值了。因此,每次运行一次lambda表达式,就会取最新的x值来绑定计算。 如果你想让某个匿名函数(lambda)在定义时捕获到值,则只要将那个参数值定义成默认值参数(也就是使用函数默认值参数定义的形式)即可。如:
1
2
3
4
5
6
7
8
>>> x = 10
>>> a = lambda y, x=x:x+y
>>> x = 20
>>> b = lambda y, x=x:x+y
>>> a(10)
20
>>> b(10)
30
扩展阅读: * Default_Parameter * 定义有默认参数的函数 * 匿名函数捕获变量值

Descriptors

他们是一大堆Python核心特性后面的魔法~~ 当你使用"."访问的形式来查找一个成员(例如,x.y),Python首先查找实例字典中的成员。如果找不到,则在类字典中查找。如果在类字典中找到了,而对象实现了描述符(descriptor)协议,Python会执行它,而不是仅仅返回它。一个描述符是任何实现__get__, __set__或者__delete__方法的类。 下面是如何使用描述符实现你自己的属性的(只读)版本:

1
2
3
4
5
6
7
8
class Property(object):
def __init__(self, fget):
self.fget = fget

def __get__(self, obj, type):
if obj is None:
return self
return self.fget(obj)
而你可以像内建的property()属性一样使用它:
1
2
3
4
class MyClass(object):
@Property
def foo(self):
return "Foo!"
Python中使用描述符来在其他东东中实现属性,绑定方法,静态方法,类方法和slot。了解它们可以很容易看到之前看起来像Python的“怪癖”的东东为什么会是那样。 Raymond Hettinger有一个很棒的教程极好的描述了描述符这个东东。

碎碎念

装饰器和描述符是两个不一样的东东哦~ 扩展阅读: * Python descriptor * Python描述符(descriptor)解密

Dictionary default .get value

字典有一个get()方法。若你使用d['key']而key不存在,那么你将会得到一个异常。而如果使用d.get('key'),那么当key不存在时,会返回None。当然.get()方法提供给了第二个参数,若此参数设了值,那么当key不存在的时候,会返回你所制定的值。例如,d.get('key',0),当d['key']不存在时,返回0. 这对某些操作很有用,列入,给数字做加法:

1
sum[value] = sum.get(value, 0) + 1
### 碎碎念 从前,有个函数,叫做setdefault。d.setdefault('key',0)表示,若d['key']存在,则返回d['key'];否则,设置d['key'] = 0。

Docstring Tests

Doctest: 文档和单元测试同时进行 从Python文档中抽取的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def factorial(n):
"""Return the factorial of n, an exact integer >= 0.

If the result is small enough to fit in an int, return an int.
Else return a long.

>>> [factorial(n) for n in range(6)]
[1, 1, 2, 6, 24, 120]
>>> factorial(-1)
Traceback (most recent call last):
...
ValueError: n must be >= 0

Factorials of floats are OK, but the float must be an exact integer:
"""

import math
if not n >= 0:
raise ValueError("n must be >= 0")
if math.floor(n) != n:
raise ValueError("n must be exact integer")
if n+1 == n: # catch a value like 1e300
raise OverflowError("n too large")
result = 1
factor = 2
while factor <= n:
result *= factor
factor += 1
return result

def _test():
import doctest
doctest.testmod()

if __name__ == "__main__":
_test()
### 碎碎念 doctest会在docstring部分加入测试代码,这就是所谓的文章和单元测试同步进行。如果doctest通过,则不会有任何输出。 好处呢,是可以用来进行回归测试,而且,直接上示例比用文字说明更直观。另外,还可以用来确认这个docString有木有过期。

Ellipsis Slicing Syntax

Python高级切片操作有一个罕为人知的语法元素:省略(Ellipsis):

1
2
3
4
5
6
>>> class C(object):
... def __getitem__(self, item):
... return item
...
>>> C()[1:2, ..., 3]
(slice(1, 2, None), Ellipsis, 3)
不幸的是,这个特性很少有用,因为只有在包含元祖的时候才支持省略。

碎碎念

这个特性真是令人困惑呢~~ 一般会出现在numpy或者scipy模块中,几个例子体会一下:

1
2
3
4
5
6
7
8
9
10
>>> from numpy import *
>>> a = array([[1,2,3],[3,4,5],[5,6,7]])
>>> print a
[[1 2 3]
[3 4 5]
[5 6 7]]
>>> print a[...,0]
[1 3 5]
>>> print a[0,...,0] # a[0,...,0] == a[0][0]
1
至于Ellipsis会输出神马,就要看所定义的__getitem__

Enumeration

用enumerate包住一个可迭代对象,它将会将index和item绑在一起,返回一个enumerate对象。 例如:

1
2
3
4
5
6
7
8
9
>>> a = ['a', 'b', 'c', 'd', 'e']
>>> for index, item in enumerate(a): print index, item
...
0 a
1 b
2 c
3 d
4 e
>>>
参考: * Python tutorial—looping techniques * Python docs—built-in functions—enumerate * PEP 279

碎碎念

这个当同时需要索引和值的时候灰常有用。 比如说,当你想这样做的时候:

1
for i in range(len(a)): print i, a[i]
不妨换成上面这种高大上的写法,会显得更python。 当然, enumerate提供第二个参数,可以指定索引开始的位置。例如,enumereate(a,1)

For/else

for...else语法如下:

1
2
3
4
5
for i in foo:
if i == 0:
break
else:
print("i was never 0")
"else"代码块会在循环正常结束后执行,但如果循环中的break语句被调用,则else代码块不会被执行。 上面的代码也等价于
1
2
3
4
5
6
7
found = False
for i in foo:
if i == 0:
found = True
break
if not found:
print("i was never 0")
### 碎碎念 这个语法其实挺容易让人认为如果循环体从不被执行的情况下,else语句应该被执行。因此,在使用的时候,最好还是注释下,免得被其他小伙伴看到搞错它的含义。

上面的代码也等价于

1
2
3
4
5
found = False
if any(i == 0 for i in foo):
pass
else:
print("i was never 0")
同类语法有while...else。

Function as iter() argument

iter()接收一个可回调参数。 例如:

1
2
3
def seek_next_line(f):
for c in iter(lambda: f.read(1),'\n'):
pass
until_value)```函数重复调用callable,然后yield结果,直到返回until_value。
1
2
3
4
5
6
7
8
9
10
11

### 碎碎念
函数说明:
```python
iter(...)
iter(collection) -> iterator
iter(callable, sentinel) -> iterator

Get an iterator from an object. In the first form, the argument must
supply its own iterator, or be a sequence.
In the second form, the callable is called until it returns the sentinel.

Generator expressions

假如你像下面这样写:

1
x=(n for n in foo if bar(n))
你将获得一个生成器,并可以把它付给一个变量x。现在,你可以像下面这样玩它了:
1
for n in x:
这样做得好处是,你不需要中间存储,而如果你像下面这样做,则需要:
1
2
x = [n for n in foo if bar(n)]
#列表推导
在某些情况下,这会导致极重要的速度提升。 你可以添加许多if语句到生成器的尾端,基本复制for循环嵌套:
1
2
3
4
5
6
7
8
>>> n = ((a,b) for a in range(0,2) for b in range(4,6))
>>> for i in n:
... print i

(0, 4)
(0, 5)
(1, 4)
(1, 5)
### 碎碎念 使用生成器最大的好处就是节省内存了。因为每一个值只有在你需要的时候才会生成,而不像列表推导那样一次性生成所有的结果。储存一个值和储存一堆值,比比看,就知道谁更省空间了。 但素,要注意的一点是,因为生成器的这个特性,所以它只能被用一次哦,这个就是所谓的"no rewind"特性。 扩展阅读:Generator Tricks for Systems Programmers

import this

1
2
import this
# btw look at this module's source :)

结果是(翻译见The Zen of Python): The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

碎碎念

前面from __future__ ipmort braces提到了这个import this。现在就让我们来看看会发生神马吧。 从这个模块的源代码来看(位于python目录下的Lib/this.py),这个是对一段加密后的s进行解密操作(ROT13),然后输出。

从源码来看,这个彩蛋是很令人困惑的。至于为什么这样写,有人说,只是个玩笑,因为这个源码本身就违反了Zen of Python。你觉得呢?

In Place Value Swapping

1
2
3
4
5
6
7
8
>>> a = 10
>>> b = 5
>>> a, b
(10, 5)

>>> a, b = b, a
>>> a, b
(5, 10)

等号右边是一个表达式,它创建了一个新的元组。等号左边立即将这个未引用的元组拆封赋值给a和b 赋值后,新的元组属于未被引用,并标记为可回收垃圾,而a和b绑定的值则被互换。 注意在Python tutorial section on data structures中有, 注意,多重赋值其实只是元组封装和序列拆封的组合。

碎碎念

此时,有小伙伴提出疑问,这会不会被传统的方式使用更多的内存空间呢? 答案是,不会!

请言小午吃个甜筒~~